using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Xml.Linq;
using BSDS.Core.Model;
using BSDS.Core.Model.Data;
using BSDS.Core.Model.Services.Jobs;
using BSDS.Core.Model.Services.Jobs.Core;

namespace BSDS.Core.Data.Remote.Geocoding.Parsing
{
    public class GeoResponseParser
    {
        public JobState ParseJobStatusResponse(string jobId, string response)
        {
            const string xmlns = "http://schemas.microsoft.com/search/local/ws/rest/v1";

            var document = XDocument.Parse(response);

            var statusValue = document.Descendants(XName.Get("DataflowJob", xmlns))
                           .Select(l => l.Element(XName.Get("Status", xmlns)))
                           .First().Value;
            var successLink = GetLink(document, xmlns, "succeeded");
            var failureLink = GetLink(document, xmlns, "failed");
            DateTime? completedDate = GetCompletionDate(document, xmlns);

            var status = (JobStatus) Enum.Parse(typeof (JobStatus), statusValue);

            var complitionStatus = successLink == null ? JobComplitionStatus.Failure : JobComplitionStatus.Success;
            var result = new JobResult(complitionStatus, successLink, failureLink);

            return new JobState(jobId, status, result, status == JobStatus.Aborted ? response : string.Empty, completedDate);
        }

        private static string GetLink(XDocument document, string xmlns, string name)
        {
            return document.Descendants(XName.Get("Link", xmlns))
                .Where(l => l.Attributes("name").Count() > 0 && l.Attribute("name").Value == name)
                .Select(e => e.Value)
                .FirstOrDefault();
        }

        private static DateTime? GetCompletionDate(XDocument document, string xmlns)
        {
            var completedDate = document.Descendants(XName.Get("DataflowJob", xmlns))
                .Select(l => l.Element(XName.Get("CompletedDate", xmlns)))
                .First().Value;

            if (string.IsNullOrEmpty(completedDate))
            {
                return null;
            }

            return DateTime.Parse(completedDate, CultureInfo.InvariantCulture);
        }

        public GeocodeJob ParseCreateGeocodingJobResponse(string response)
        {
            const string xmlns = "http://schemas.microsoft.com/search/local/ws/rest/v1";

            var document = XDocument.Parse(response);

            return document.Descendants(XName.Get("DataflowJob", xmlns))
                           .Select(job => new GeocodeJob
                           {
                               Id = job.Element(XName.Get("Id", xmlns)).Value.Trim(),
                               Description = job.Element(XName.Get("Description", xmlns)).Value.Trim(),
                               Link = job.Element(XName.Get("Link", xmlns)).Value.Trim(),
                               SubmissionDate = DateTime.Parse(job.Element(XName.Get("CreatedDate", xmlns)).Value.Trim())
                           }).First();
        }

        public EntityValue[] ParseGetGeocodingResultResponse(string response)
        {
            const string xmlns = "http://schemas.microsoft.com/search/local/2010/5/geocode";

            var document = XDocument.Parse(response);

            return document.Descendants(XName.Get("GeocodeEntity", xmlns))
                .Select(ge =>
                    {
                        var entityAttribute = ge.Attribute(XName.Get("Id"));
                        return ParseToEntity(entityAttribute, ge.Element(XName.Get("GeocodeResponse", xmlns)));
                    })
                    .ToArray();
        }

        private static EntityValue ParseToEntity(XAttribute entityId, XElement responseElement)
        {
            const string xmlns = "http://schemas.microsoft.com/search/local/2010/5/geocode";

            var attributes = new List<AttributeValue>
                                 {
                                     new AttributeValue(new AttributeInfo("EntityId", "long", true), entityId.Value)
                                 };

            Action<XAttribute> createAttributeValue = a =>
                    {
                        var attributeInfo = new AttributeInfo(a.Name.LocalName,"string", a.Name.LocalName =="Id");

                        var existingAttribute = attributes.FirstOrDefault(ea => ea.AttributeInfo.Name == a.Name.LocalName);

                        if (existingAttribute == null)
                        {
                            attributes.Add(new AttributeValue(attributeInfo, a.Value.Trim()));
                        }
                        else
                        {
                            existingAttribute.Value = a.Value.Trim(); // This will allow RooftopLocation Lat/Lon values to replace Interpolated Lat/Lon values
                        }
                    };

            responseElement.Attributes().ToList().ForEach(createAttributeValue);
            responseElement.Descendants(XName.Get("Address", xmlns)).Attributes().ToList().ForEach(createAttributeValue);
            responseElement.Descendants(XName.Get("InterpolatedLocation", xmlns)).Attributes().ToList().ForEach(createAttributeValue);
            responseElement.Descendants(XName.Get("RooftopLocation", xmlns)).Attributes().ToList().ForEach(createAttributeValue); // Rooftop is preferred so make sure this is parsed last
            
            UpdateGeocodePrecision(attributes);
            FixBadRequest(attributes);
            CreateMatchCodeEquivalence(attributes);
            CreateMatchedMethodEquivalence(attributes);

            attributes.Add(new AttributeValue(new AttributeInfo("Geocoder", "string", false), "BSDS Dataflow API"));

            return new EntityValue(attributes);
        }


        #region [ public static method ]

        public static void UpdateGeocodePrecision(List<AttributeValue> attributes)
        {
            string[] geocodeAttributes = { "Latitude", "Longitude" };

            foreach (string s in geocodeAttributes)
            {
                var attribute = attributes.FirstOrDefault(x => x.AttributeInfo.Name == s);
                if (attribute != null)
                {
                    double l;
                    if (double.TryParse(attribute.Value, out l))
                        attribute.Value = l.ToString("0.000000");
                }
            }
        }

        public static void FixBadRequest(List<AttributeValue> attributes)
        {
            if (!attributes.Any(x => x.AttributeInfo.Name == "Latitude"))
            {
                attributes.Add(new AttributeValue(new AttributeInfo("Latitude", "string", false), "0"));
            }

            if (!attributes.Any(x => x.AttributeInfo.Name == "Longitude"))
            {
                attributes.Add(new AttributeValue(new AttributeInfo("Longitude", "string", false), "0"));
            }

            if (!attributes.Any(x => x.AttributeInfo.Name == "Confidence"))
            {
                attributes.Add(new AttributeValue(new AttributeInfo("Confidence", "string", false), "BadRequest"));
                var statusCode = attributes.FirstOrDefault(x => x.AttributeInfo.Name == "StatusCode");
                if (statusCode != null)
                {
                    statusCode.Value = "Failed"; // This fixes a bug in the geocode response where "Success" is always returned.
                }
            }
        }

        public static void CreateMatchCodeEquivalence(List<AttributeValue> attributes)
        {
            var confidence = attributes.FirstOrDefault(x => x.AttributeInfo.Name == "Confidence");

            string entityType = String.Empty;
            if (attributes.Any(x => x.AttributeInfo.Name == "EntityType"))
                entityType = attributes.FirstOrDefault(x => x.AttributeInfo.Name == "EntityType").Value;

            string matchCode = String.Empty;
            if (confidence != null)
            {
                switch (confidence.Value)
                {
                    case "High"      : matchCode = "Good"; break;
                    case "Medium"    : matchCode = entityType == "Address" ? "Ambiguous" : "Imprecise"; break;
                    case "Low"       : matchCode = "Imprecise"; break; 
                    case "BadRequest": matchCode = "Bad"; break;

                    // TODO: Set to ambiguous if confidence is medium and match method is not set to address
                }
            }
            
            attributes.Add(new AttributeValue(new AttributeInfo("MatchCode", "string", false), matchCode));
        }

        public static void CreateMatchedMethodEquivalence(List<AttributeValue> attributes)
        {
            string entityType = String.Empty;
            string matchedMethod = String.Empty;

            if (attributes.Any(x => x.AttributeInfo.Name == "EntityType"))
                entityType = attributes.FirstOrDefault(x => x.AttributeInfo.Name == "EntityType").Value;

            if (entityType == "RoadBlock")
                matchedMethod = "Street";        // Street (not sure why RoadBlock is used when street number is missing)
            else if (attributes.Any(x => x.AttributeInfo.Name == "AddressLine"))
                matchedMethod = "Address";       // Address (should contain street number if entityType is not RoadBlock)
            else if (attributes.Any(x => x.AttributeInfo.Name == "PostalCode"))
                matchedMethod = "PostalCode";    // Zip
            else if (attributes.Any(x => x.AttributeInfo.Name == "Locality"))
                matchedMethod = "Locality";      // City (we will not use PrimaryCity like Mappoint)
            else if (attributes.Any(x => x.AttributeInfo.Name == "AdminDistrict"))
                matchedMethod = "AdminDistrict"; // State (we will not use Subdivision like Mappoint)
            else if (attributes.Any(x => x.AttributeInfo.Name == "CountryRegion"))
                matchedMethod = "CountryRegion"; // Country
            else
                matchedMethod = entityType;

            attributes.Add(new AttributeValue(new AttributeInfo("MatchedMethod", "string", false), matchedMethod));
        }

        #endregion
    }
}